[Android Tips] AsyncTaskLoaderをTestする
AsyncTaskLoaderをテストしたい
非同期処理はテストしづらいですね。 AsyncTaskLoaderのテストをする際にはまったのでメモしておきます。いわゆる俺得エントリー
準備するもの
レシピには以下のものが必要になります
- AsyncTaskLoaderのクラスを使った非同期処理があるAndroidプロジェクト
- テストしたいプロジェクトを対象にしたAndroid Testプロジェクト
- 諦めない心
今回利用するソースコードはこちら。 AsyncTaskSampleとAsyncTaskSampleTestの2つのプロジェクトを利用します
今回は、文字列から緯度経度を検索する非同期処理を行う簡単なAndroidプロジェクトを対象に、テストプロジェクトを作成します。AsyncTaskLoaderの拡張であるGeocoderLoaderは以下の通りです。
/** * 住所や施設名などから緯度経度情報を取得する、Geocoder#getFromLocationName() * を非同期で実施するAsyncTaskLoaderクラス。 * */ public class GeocoderLoader extends AsyncTaskLoader<LatLng> { private Context context; private String string; /** * Constructor * * @param context {@link Context} * @param str 検索する住所 */ public GeocoderLoader(Context context, String string) { super(context); this.context = context; this.string = string; } /** LatLngを返却 */ @Override public LatLng loadInBackground() { LatLng latLng = null; // 検索バーからの住所検索処理 Geocoder geocoder = new Geocoder(context, Locale.getDefault()); try { // 住所を1件取得する List<Address> addressList; addressList = geocoder.getFromLocationName(string, 1); Address address = addressList.get(0); double lat = address.getLatitude(); double lng = address.getLongitude(); // 取得した住所からLatLng作成 latLng = new LatLng(lat, lng); } catch (IOException e) { e.printStackTrace(); Log.d("", "geocoder Error: " + e.toString()); } return latLng; } }
LatLngクラスは、元々Google Play Servicesの中にあるクラスですが、面倒なので簡易的なクラスを作成しました。気を付けておくのは、ここで利用するAsyncTaskLoaderはandroid.support.v4.content.AsyncTaskLoaderのものです
public class LatLng { public double lat; public double lng; public LatLng(double lat, double lng) { this.lat = lat; this.lng = lng; } @Override public String toString() { return "lat:" + lat + ", lng:" + lng; } }
テストプロジェクト
さて、ここからが本番です。上記のGeocoderLoaderをテストしていきましょう。 まずは素直に書いてみます
NGパターン1
/** * GeocoderLoaderをテストする * @author komuro.hiraku * */ public class TestGeocoderLoader extends ActivityInstrumentationTestCase2<MainActivity> { private MainActivity mActivity; public TestGeocoderLoader(Class<MainActivity> activityClass) { super(activityClass); } public TestGeocoderLoader() { super(MainActivity.class); } @Override protected void setUp() throws Exception { super.setUp(); // Activityを取得 mActivity = getActivity(); } /** * Geocoderで「東京タワー」の緯度経度を検索する処理をテストする * @throws InterruptedException */ public void testGeocoder正常_東京タワー() throws InterruptedException { // 検索対象文字列 final String TARGET = "東京タワー"; // Loaderの結果を受けるためのCallbackを作成 final LoaderCallbacks<LatLng> callback = new LoaderCallbacks<LatLng>() { @Override public Loader<LatLng> onCreateLoader(int id, Bundle args) { // GeocoderLoaderを作成 return new GeocoderLoader(mActivity, TARGET); } @Override public void onLoadFinished(Loader<LatLng> loader, LatLng data) { // 東京タワーの位置は「35.6585805, 139.7454329」 LatLng expect = new LatLng(35.6585805, 139.7454329); assertEquals("東京タワーの位置が合致しました", expect.toString(), data.toString()); } @Override public void onLoaderReset(Loader<LatLng> loader) { // Resetは今回は考慮しない } }; // Loaderを作成 LoaderManager lm = mActivity.getLoaderManager(); Loader<LatLng> loader = lm.initLoader(0, null, callback); // Loaderを実行 loader.forceLoad(); } }
これは当然NGパターンです。なぜなら、assertEqualsの期待値の値を何に変えてもグリーンでテストを通過してしまいます。 これは、loaderの起動直後にメソッドから抜けてしまうため、何もAssertせずにメソッドを無事通過してしまうためです。 onLoadFinishedを実行しようとしたときには、すでにテスト後のため、当然処理の結果は受け取れません。 きちんと処理完了まで待ってあげましょう
NGパターン2
非同期通信完了まで、待つためにCountDownLatchを導入します。 詳しくはこちらを参照してください。 非同期処理の待ち合わせ機構です。
/** * GeocoderLoaderをテストする * @author komuro.hiraku * */ public class TestGeocoderLoader extends ActivityInstrumentationTestCase2<MainActivity> { private MainActivity mActivity; public TestGeocoderLoader(Class<MainActivity> activityClass) { super(activityClass); } public TestGeocoderLoader() { super(MainActivity.class); } @Override protected void setUp() throws Exception { super.setUp(); // Activityを取得 mActivity = getActivity(); } /** * Geocoderで「東京タワー」の緯度経度を検索する処理をテストする * @throws InterruptedException */ public void testGeocoder正常_東京タワー() throws InterruptedException { // 非同期処理を待つためのLatch。 // 待機するのは非同期処理一本だけなので引数は1で作成 final CountDownLatch latch = new CountDownLatch(1); // 検索対象文字列 final String TARGET = "東京タワー"; // Loaderの結果を受けるためのCallbackを作成 final LoaderCallbacks<LatLng> callback = new LoaderCallbacks<LatLng>() { @Override public Loader<LatLng> onCreateLoader(int id, Bundle args) { // GeocoderLoaderを作成 return new GeocoderLoaderV4(mActivity, TARGET); } @Override public void onLoadFinished(Loader<LatLng> loader, LatLng data) { // 東京タワーの位置は「35.6585805, 139.7454329」 LatLng expect = new LatLng(35.6585805, 139.7454329); assertEquals("東京タワーの位置が合致しました", expect.toString(), data.toString()); // 非同期通信が完了したので、処理を続行 latch.countDown(); } @Override public void onLoaderReset(Loader<LatLng> loader) { // Resetは今回は考慮しない } }; // Loaderを作成 LoaderManager lm = mActivity.getSupportLoaderManager(); Loader<LatLng> loader = lm.initLoader(0, null, callback); // Loaderを実行 loader.forceLoad(); // 非同期通信が完了するまで10秒待機 boolean res = latch.await(10, TimeUnit.SECONDS); assertEquals("通信完了", true, res); } }
CountDownLatchを導入し、onLoadFinishedまで待機するようにしました。 一応、10秒間のタイムアウトを設定しています。
さて、これで一見いいように思えるのですが、実行してみると失敗します。
失敗しました。 どうやらonLoadFinishedが呼ばれずにlatch.await()がTimeoutしているようです
メインスレッドがawait()でブロックされているため、メインスレッドのキューがいつまでも実行されずにブロックされているのではないかと推測しました。 さらに別スレッドで実行させるよう試みてみます。
OKパターン
結論としては、前述した推測が正解だったようです。 v4のAsyncTaskLoaderの動作をテストしたい場合は、下記のように記述すればOKです
/** * Geocoderで「東京タワー」の緯度経度を検索する処理をテストする * @throws InterruptedException */ public void testGeocoder正常_東京タワー() throws InterruptedException { // 非同期処理を待つためのLatch。 // 待機するのは非同期処理一本だけなので引数は1で作成 final CountDownLatch latch = new CountDownLatch(1); // 検索対象文字列 final String TARGET = "東京タワー"; // Loaderの結果を受けるためのCallbackを作成 final LoaderCallbacks<LatLng> callback = new LoaderCallbacks<LatLng>() { @Override public Loader<LatLng> onCreateLoader(int id, Bundle args) { // GeocoderLoaderを作成 return new GeocoderLoaderV4(mActivity, TARGET); } @Override public void onLoadFinished(Loader<LatLng> loader, LatLng data) { // 東京タワーの位置は「35.6585805, 139.7454329」 LatLng expect = new LatLng(35.6585805, 139.7454329); assertEquals("東京タワーの位置が合致しました", expect.toString(), data.toString()); // 非同期通信が完了したので、処理を続行 latch.countDown(); } @Override public void onLoaderReset(Loader<LatLng> loader) { // Resetは今回は考慮しない } }; mActivity.runOnUiThread(new Runnable(){ @Override public void run() { // Loaderを作成 LoaderManager lm = mActivity.getSupportLoaderManager(); Loader<LatLng> loader = lm.initLoader(0, null, callback); // Loaderを実行 loader.forceLoad(); } }); // 非同期通信が完了するまで10秒待機 boolean res = latch.await(10, TimeUnit.SECONDS); assertEquals("通信完了", true, res); }
ActivityのrunOnUiThread上で別スレッドとして呼び出す必要があります。 こうするときちんとonLoadFinishedまで通過し、テストが無事完了します。
完了!
AsyncTaskLoaderをテストする時のポイント
ポイントは、AsyncTaskLoaderが非同期処理であることを意識することです。 「そんなの当たり前だろ!」と方々から矢が飛んできそうですが、意外と忘れがちです。
また、Support LibraryのAsyncTaskLoaderかAsyncTaskLoaderクラスかでも動作が大きく異なるので注意しましょう。
まとめ
なぜか、NGパターン2の実装で動かなかったので、不思議に思って色々調べた結果です。AsyncTaskLoaderをテストする場合は色々注意が必要のようです。 日本語の情報がさっぱり載っていなかったので、みなさんどのようにテストしているのでしょうか。 当たり前すぎて出てこない情報なんでしょうか。
さらに実験してる最中に気づきましたが、v4パッケージのAsyncTaskLoaderと通常のAsyncTaskLoaderでは動作が違うようです。 これはまた別エントリで解説しようと思います
それでは皆様ごきげんよう